模拟器检测 & 反调试

模拟器检测 & 反调试

1 模拟器检测

具体参考:检测Android虚拟机的方法和代码实现

简单列举一下:

  1. 根据特征检测
  2. Device ID
  3. Default Number
  4. IMSI
  5. Buid类
  6. 运营商名称
  7. QEMU驱动
  8. QEMU文件
  9. Genymotion文件
  10. QEMU管道

2 反调试

2.1 IDA端口检测

远程调试会占据一些固定的端口号

/proc/net/tcp查找IDA所用的端口23946,如果发现说明进程正在被IDA调试

netstat -apn结果中搜索23946

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void CheckPort23946ByTcp() {
FILE* pfile = null;
char buf[0x10000] = {0};
//执行命令
char* strCatTcp = "cat /proc/net/tcp | grep : 5D8A";
// char* strNetstat = "netstat | grep :23946";
pfile = popen(strCatTcp, "r");
if (NULL == pfile) {
LOGA("CheckPort23946ByTcp popen打开命令失败!\n");
return ;
}
// 获取结果
while (fgets(buf, sizeof(buf), pfile)) {
// 执行到这里,判定为调试状态
LOGA("执行cat /proc/net/tcp |grep :5D8A的结果:\n");
LOGB("%s",buf);
}
}

2.2 调试器进程名称检测

调试的时候会在手机中运行特定的进程 如:

android_server

gdbserver

gdb

做法:遍历进程名,找到则说明调试器在运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void SearchObjProcess() {     
FILE* pfile=NULL;
char buf[0x1000]={0};
// 执行命令
//pfile=popen("ps | awk '{print $9}'","r");
// 部分不支持awk命令
pfile=popen("ps","r");
if(NULL==pfile) {
LOGA("SearchObjProcess popen打开命令失败!\n");
return;
}
// 获取结果
LOGA("popen方案:\n");
while(fgets(buf,sizeof(buf),pfile)) {
// 打印进程
LOGB("遍历进程:%s\n",buf);
// 查找子串
char* strA=NULL,strB=NULL,strC=NULL,strD=NULL;
strA=strstr(buf,"android_server");
strB=strstr(buf,"gdbserver");
strC=strstr(buf,"gdb");
strD=strstr(buf,"fuwu");
if(strA || strB ||strC || strD) {
// 执行到这里,判定为调试状态
LOGB("发现目标进程:%s\n",buf);
}
}
pclose(pfile);
}

如果不是使用apk附加调试,而是单独写一个程序,加载so文件进行调试

2.3 父进程检测

那么,程序的父进程名称和正常启动apk的父进程名称是不一样的。

  1. 正常启动的apk程序父进程名称都是zygote
  2. 调试启动的apk父进程也还是zygote
  3. 附加调试的apk程序父进程是zygote
  4. vs远程调试可执行文件加载so 父进程名为gdbserver

2.4 自身进程检测

名字不是com.xxxx

2.5 apk 线程检测

正常apk线程个数比较多

自写可执行程序线程单一

/proc/{pid}/task

2.6 apk进程fd文件检测

根据/proc/pid/fd下文件个数的差异,判断进程的状态

2.7 安卓系统自带调试函数检测

检测调试:

1
2
3
public static boolean isBeingDebugged() {
return Debug.isDebuggerConnected();
}

2.8 ptrace检测是否有程序跟踪:

进程下status第6行不为0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private static String tracerpid = "TracerPid";
/**
* 阿里巴巴用于检测是否在跟踪应用进程
* 容易规避, 用法是创建一个线程每3秒检测一次, 如果检测到则程序崩溃
* @return
* @throws IOException
*/
public static boolean hasTracerPid() throws IOException {
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(new FileInputStream("/proc/self/status")), 1000);
String line;

while ((line = reader.readLine()) != null) {
if (line.length() > tracerpid.length()) {
if (line.substring(0, tracerpid.length()).equalsIgnoreCase(tracerpid)) {
if (Integer.decode(line.substring(tracerpid.length() + 1).trim()) > 0) {
return true;
}
break;
}
}
}

} catch (Exception exception) {
exception.printStackTrace();
} finally {
reader.close();
}
return false;
}

2.9 函数hash值检测

so文件中函数的指令是固定,但是如果被下了软件断点,指令就会发生改变(断点地址被改 写为bkpt断点指令),可以计算内存中一段指令的hash值进行校验,检测函数是否被修改或被下断点

2.10 断点指令检测

可以在函数体中搜索bkpt指令来检测软件断电。

2.11 系统源码修改检测

2.12 单步调试陷阱

原理:

调试器调试下断点到执行断点过程如下:

  1. 保存: 保存目标处指令

  2. 替换: 替换目标处指令为断点指令

  3. 执行中断: 引发中断

调试器收到信号之后,执行调试器注册的信号处理函数

  1. 恢复:调试器处理函数恢复保存的指令
  2. 回退:回退PC寄存器
  3. 控制权回归程序

反调试方案:

在代码中注册断点信号处理函数

在非调试状态下:

进入自己注册的函数,NOP掉断掉指令,回退PC后正常指令

在调试状态下:

进入调试器的断点处理流程,会恢复目标指令失败,回退PC进入死循环

2.13 利用IDA先截获中断特性的检测

IDA会首先截获信号,导致进程无法接收到信号,导致不会执行信号处理函数。将关键流程 放在信号处理函数中,如果没有执行,就是被调试状态

2.14 执行时间检测

正常情况下:在a地点获取执行时间,运行一段时间后,在b处获取时间,然后求时间差,这个差会比较小。

调试状态下: 如果存在调试,那么这个时间差会比较大

2.15 进程信息结构检测

/proc/pid/status

/proc/pid/task/pid/status

TracerPid != 0

statue字段写入了t(tracing stop)

/proc/pid/stat

proc/pid/task/pid/stat

第二个字段为t

/proc/pid/wchan

/proc/pid/task/pid/wchan

ptrace_stop